import torch
import torch.nn as nn



dr = 0.5


class BasicBlock(nn.Module):
    """Basic Block for resnet 18 and resnet 34
    """

    #BasicBlock and BottleNeck block
    #have different output size
    #we use class attribute expansion
    #to distinct
    expansion = 1

    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()

        #residual function
        self.residual_function = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels * BasicBlock.expansion, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_channels * BasicBlock.expansion)
        )

        #shortcut
        self.shortcut = nn.Sequential()

        #the shortcut output dimension is not the same with residual function
        #use 1*1 convolution to match the dimension
        if stride != 1 or in_channels != BasicBlock.expansion * out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels * BasicBlock.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * BasicBlock.expansion)
            )

    def forward(self, x):
        return nn.ReLU(inplace=True)(self.residual_function(x) + self.shortcut(x))

class BottleNeck(nn.Module):
    """Residual block for resnet over 50 layers
    """
    expansion = 4
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.residual_function = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, stride=stride, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels * BottleNeck.expansion, kernel_size=1, bias=False),
            nn.BatchNorm2d(out_channels * BottleNeck.expansion),
        )

        self.shortcut = nn.Sequential()

        if stride != 1 or in_channels != out_channels * BottleNeck.expansion:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels * BottleNeck.expansion, stride=stride, kernel_size=1, bias=False),
                nn.BatchNorm2d(out_channels * BottleNeck.expansion)
            )

    def forward(self, x):
        return nn.ReLU(inplace=True)(self.residual_function(x) + self.shortcut(x))

class ResNet_F(nn.Module):
    """Modulation Recognition ResNet (Mine)"""

    def __init__(
            self,
            block,
            num_block,
            ori_channels: int = 1,
            n_filters: int = 32,
            out_channels: int = 64,
            latent_dim: int = 128,
            n_classes: int = 11
    ):
        super(ResNet_F, self).__init__()

        self.block = block
        self.n_filters = n_filters
        self.n_channels = n_filters
        self.out_ch = out_channels


        # self.preprocess = PowerNormalization()

        self.head = nn.Sequential(
            nn.Conv2d(ori_channels, n_filters, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(n_filters),
            nn.ReLU(inplace=True))

        self.layer1 = self._make_layer(self.block, self.out_ch*1, num_block[0], 1)
        self.layer2 = self._make_layer(self.block, self.out_ch*2, num_block[1], 2)
        self.layer3 = self._make_layer(self.block, self.out_ch*3, num_block[2], 2)
        self.layer4 = self._make_layer(self.block, self.out_ch*4, num_block[3], 2)

        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc1 = nn.Linear(self.n_channels, latent_dim, bias=False)
        # self.fc = nn.Linear(2, self.out_ch, bias=False)

    def _make_layer(self, block, out_channels, num_blocks, stride):
        """make resnet layers(by layer i didnt mean this 'layer' was the
        same as a neuron netowork layer, ex. conv layer), one layer may
        contain more than one residual block
        Args:
            block: block type, basic block or bottle neck block
            out_channels: output depth channel number of this layer
            num_blocks: how many blocks per layer
            stride: the stride of the first block of this layer
        Return:
            return a resnet layer
        """

        # we have num_block blocks per layer, the first block
        # could be 1 or 2, other blocks would always be 1
        strides = [stride] + [1] * (num_blocks - 1)
        layers = []
        for stride in strides:
            layers.append(block(self.n_channels, out_channels, stride))
            self.n_channels = out_channels * block.expansion
        return nn.Sequential(*layers)

    def forward(self, x):
        # x = x.view(-1, 1, 2, 128)#(-1, 1, 2, 128)
        # x = self.preprocess(x)
        x = self.head(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.avg_pool(x)
        x = x.view(x.size(0), -1)
        h = self.fc1(x)
        # out = self.fc(h)

        return h

def resnet18_F(n_classes=11, latent_dim=128):
    """ return a ResNet 18 object
    """
    return ResNet_F(BasicBlock, [2, 2, 2, 2], n_classes=n_classes, latent_dim=latent_dim)

def resnet34_F(n_classes=11, latent_dim=128):
    """ return a ResNet 34 object
    """
    return ResNet_F(BasicBlock, [3, 4, 6, 3], n_classes=n_classes, latent_dim=latent_dim)

def resnet50_F(n_classes=11, latent_dim=128):
    """ return a ResNet 50 object
    """
    return ResNet_F(BottleNeck, [3, 4, 6, 3], n_classes=n_classes, latent_dim=latent_dim)

def resnet101_F(n_classes=11, latent_dim=128):
    """ return a ResNet 101 object
    """
    return ResNet_F(BottleNeck, [3, 4, 23, 3], n_classes=n_classes, latent_dim=latent_dim)

def resnet152_F(n_classes=11, latent_dim=128):
    """ return a ResNet 152 object
    """
    return ResNet_F(BottleNeck, [3, 8, 36, 3], n_classes=n_classes, latent_dim=latent_dim)


